<?php
namespace DRY\Database;

// TODO: Investigate configuring this driver to be able to return objects for result rows instead of arrays, as stdClass or as an instance of an existing class (fetch_object)
/**
 * DonReY Framework 2012 :: Database driver for MySQLi extension
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/database.html#mysqli
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Database
 */
class Driver_mysqli extends \mysqli implements Driver_base
{

	/**
	 * The Database module which instantiated this driver
	 * @var \DRY\Module\Database
	 */
	private $database;

	/**
	 * True if connection is persistent
	 * @var bool
	 */
	private $persistent = false;

	/**
	 * True if connection was succesful
	 * @var bool
	 */
	private $connected = false;

	/**
	 * Connection debugging mode:
	 * - 0 = no debugging at all ( production mode )
	 * - 1 = record all queries
	 * - 2 = record queries and mysql's error reports (when there is a problem)
	 * - 3 - record queries, errors or statistics for successful queries ( affected rows, result size, etc )
	 * @var string
	 */
	private $debug = false;

	/**
	 * Extended constructor, allows sending config data directly from app config (object/simplexml)
	 * @param SimpleXMLElement $CFG
	 * @param \DRY\Module\Database $database The Database module which instantiated this driver
	 */
	public function __construct($CFG, \DRY\Module\Database &$database)
	{
		$this-> persistent = (bool)$CFG-> driver['persistent'];
		$this-> debug = (int)$CFG-> driver['debug'];
		$this-> database = &$database;

		@parent::__construct(($this-> persistent?'p:':'').$CFG-> server,$CFG-> username,$CFG-> password,$CFG-> dbname,$CFG-> port?:ini_get("mysqli.default_port"));

		// check if connection failed
		if($this->connect_errno) {
			if($this-> debug >= 2)
				\DRY\Water::dump($this-> connect_error, 'Database::MySQLi Connect Error '.$this-> connect_errno, null, 'warning');
		} else {
			if($this-> debug >= 3)
				\DRY\Water::dump("Connection estabilished", 'Database::MySQLi', null, 'notice');
			$this-> set_charset("utf8");
			$this-> connected = true;
		}
	}

	/**
	 * Destructor, cleans up and closes database connection if non-persistent
	 */
	public function __destruct()
	{
		if(!$this-> persistent && $this-> connected) {
			$this-> close();
			if($this-> debug >= 3)
				\DRY\Water::dump("Connection closed", 'Database::MySQLi', null, 'notice');
		}
	}

	/**
	 * Returns if the connection was successful or not
	 */
	public function is_Connected()
	{
		return $this-> connected;
	}

	/**
	 * Status of the SELECT cache (0 = no caching, 1 = only next, 2 = active for all until disabled)
	 * @var int
	 */
	private $cache_stat;

	/**
	 * Callback function to be run on the result set before the data is cached. Necessary to support split_by_prefixes, index_field ( and perhaps others in the future )
	 * @var unknown_type
	 */
	private $cache_result_setparams;

	/**
	 * Activates caching on the following SELECT queries
	 * @param int $expire
	 */
	public function cache_On($expire = 60, $result_setparams = null)
	{
		if(!isset($this-> database['cache']))
			return false;
		$this-> cache_stat = 2;
		if(!is_null($expire))
			$this-> database['cache']-> expire($expire);
		$this-> cache_result_setparams = $result_setparams;
		return true;
	}

	/**
	 * Deactivates caching
	 */
	public function cache_Off()
	{
		$this-> cache_stat = 0;
	}

	/**
	 * Activates caching only on the next SELECT query
	 * @param int $expire
	 */
	public function cache_Next($expire = 60, $result_setparams = null)
	{
		if(!isset($this-> database['cache']))
			return false;
		$this-> cache_stat = 1;
		if(!is_null($expire))
			$this-> database['cache']-> expire($expire);
		$this-> cache_result_setparams = $result_setparams;
		return true;
	}

	/**
	 * Runs an SQL query and returns the result object
	 *
	 * @param string $query
	 * @param bool $simulate If true, the query isn't actually executed (just displayed in debug)
	 * @return unknown
	 */
    public function query($query, $simulate = false)
    {
    	if(!$this-> connected) // attempting to run a Query on a failed connection
    		throw new \DRY\Water('database', \DRY\Module\Database::EXC_NOT_CONNECTED_ON_QUERY);

    	$query = (string)$query;
    	if($this-> debug >= 1) {
       		// trace where the call to this query was initiated
			$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
			// identify the next trace element which has File/Line and is not "database.module.php"
			foreach($trace as &$trace_row) {
				if(isset($trace_row['file']) && substr($trace_row['file'], -19) != "database.module.php") {
					$source = array(
						'Line'=> $trace_row['line'],
						'File'=> $trace_row['file']
					);
		    		\DRY\Water::dump($query, 'Database::MySQLi Query'.($simulate?" [SIMULATED]":''), $source);
		    		break;
				}
			}
    	}
    	// allows tracing queries without actually executing them
    	if($simulate)
    		return null;

    	// Caching: test if query starts with "select" and cache it if cache_stat > 0
    	if($this-> cache_stat && isset($this-> database['cache']) && strtolower(substr($query,0,6))=='select') {
    		// test cache
    		$cache_key = md5($query);
    		$result = $this-> database['cache']-> $cache_key;
    	} else
    		$result = null;

    	if($result) {
    		if($this-> debug >= 3) {
    			\DRY\Water::dump(array(
    				'rows'=> count($result),
    				'fields'=> count(reset($result))
    			), 'Database::MySQLi [CACHED] Select Stats', $source);
    		}
    		return new \ArrayIterator($result); // caching is active and result is in cache
    	} else {
	    	// run the query !
	        $this->real_query($query);
	        if($this-> errno) {
	        	if($this-> debug >= 2)
	        		\DRY\Water::dump($this-> error, 'Database::MySQLi Error '.$this-> errno, $source, 'error');
	        	return $this-> errno;
	        } else {
	        	$result = new namespace\mysqli_result($this);
	        	// query result debug
	        	if($this-> debug >= 3) {
	        		if(stripos(ltrim($query),'select')===0 || stripos(ltrim($query),'describe')===0)
	        			\DRY\Water::dump(array(
	        				'rows'=> $result-> num_rows,
	        				'fields'=> $result-> field_count
	        			), "Database::MySQLi Select Stats");
	        		else
	        			\DRY\Water::dump($this-> get_Response(true), "Database::MySQLi Info", $source);
	        	}
	        	// if caching active, cache the result then return the cached resultSet
	        	if($this-> cache_stat && isset($this-> database['cache']) && strtolower(substr($query,0,6))=='select') {

	        		if(is_callable($this-> cache_result_setparams)) {
	        			$crsp = &$this-> cache_result_setparams;
	        			$crsp($result);
	        		}

	        		$resultSet = array();
	        		foreach($result as $key=> $row)
	        			$resultSet[$key] = $row;
	        		$this-> database['cache']-> $cache_key = $resultSet; // result is stored as simple array
	        		// if it was a one-shot, disable caching
	        		if($this-> cache_stat == 1)
	        			$this-> cache_stat = 0;
	        		return new \ArrayIterator($resultSet);
	        	} else
	        	// no caching, return the normal SQL result
	        		return $result;
	        }
    	}
    }

    /**
     * @experimental ; @todo: A way to actually plugin the cache into ::query() so caching is transparent to the modules that use this driver
     * Used for queries on data that rarely changes, attempts to grab the data from a Cache module first, and if not found, reads from the actual database (only for Selects)
     * Returns the entire result set (array)
     *
     * @param \DRY\Module\Cache $cache
     * @param mixed $query
     */
    public function cached_ResultSet(\DRY\Module\Cache $cache, $query)
    {
    	$query = (string)$query;
    	$resultSet = $cache-> $query;
    	if(!$resultSet) {
    		$r = $this-> query($query);
    		$resultSet = array();
    		foreach($r as $key=> $row)
    			$resultSet[$key] = $row;
    		$cache-> $query = $resultSet;
    	}
    	return $resultSet;
    }

    /**
     * Breaks down the information contained in mysqli-> info into an associative array
     *
     * @param bool $add_insert_id
     */
    public function get_Response($add_insert_id = false)
    {
    	$r_arr = array();
    	if(!empty($this-> info)) {
    		$response = explode('  ',$this-> info);
	    	foreach($response as $r) {
	    		list($key,$val) = explode(':',$r);
	    		$r_arr[str_replace(' ','_',strtolower($key))]=trim($val);
	    	}
    	}
    	if($add_insert_id)
    		$r_arr['last_insert_id'] = $this-> insert_id;
    	$r_arr['affected_rows'] = $this-> affected_rows;
    	return $r_arr;
    }

}

/**
 * DonReY Framework 2012 :: Extension to mysqli_result that has iterative properties and other functions for easier access to results
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Modules/database_mysqli.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version Build 000005 - 2011-11-13
 * @package DonReY
 * @subpackage Module.Database
 */
class mysqli_result extends \mysqli_result implements Driver_result_base, \Iterator, \Countable
{
	/**
	 * Current row. Used by the iterator
	 *
	 * @var array
	 */
	protected $cr_row;

	/**
	 * Current key for iterator, or the value of a field if $index_field was used
	 *
	 * @var int|string
	 */
	protected $cr_key=0;

	/**
	 * Indicates if a row is loaded into $cr_row (and that it was available from mysql)
	 *
	 * @var bool
	 */
	protected $row_loaded=false;

	/**
	 * Number of rows in the result - Used by count()
	 *
	 * @var int
	 */
	protected $row_count;

	/**
	 * Retrieves a row
	 *
	 * @return array
	 */
	public function current()
	{
		if($this->cr_key<0)
			return null; // already closed!
		if(!$this->row_loaded) {
			$this->cr_row = $this->fetch_assoc();
			if(!is_null($this->cr_row))
				$this->row_loaded=true;
		}
		return $this->cr_row;
	}

	/**
	 * Prepares the iterator for reading the next row.
	 *
	 * @return unknown
	 */
	public function next()
	{
		if($this->cr_key<0)
			return; // already closed!
		if(is_null($this->index_field))
			++$this->cr_key;
		$this->row_loaded=false; // ready for loading next
	}

	/**
	 * Returns the key of the current row ( iterator function )
	 *
	 * @return unknown
	 */
	public function key()
	{
		return $this->cr_key;
	}

	/**
	 * Checks if a row is availble. If this returns false, there are no more rows
	 *
	 * @return bool
	 */
	public function valid()
	{
		if($this->cr_key<0)
			return false; // already closed

		if(!$this->row_loaded) // attempt to load current row if none currently loaded
			$this->current();
		if($this->row_loaded)
			return true;
		else { // if the row is not loaded now, then there are no more rows, close the result and return false
			$this->close();
			return false;
		}
	}

	private $no_rewind = false;

	/**
	 * Initializes the iterator
	 */
	public function rewind()
	{
		if($this->cr_key<0 || $this-> no_rewind)
			return;
		$this->data_seek(0);
		$this->cr_key=0;
	}

	/**
	 * Allows iterators to continue results in case a previous iterator was incomplete
	 */
	public function disable_Rewind()
	{
		$this-> no_rewind = true;
	}

	/**
	 * Closes the result
	 */
	public function close()
	{
		if($this->cr_key>=0) {
			unset($this->cr_row);
			$this->cr_key=-1; // result closed, use this as indicator

			// before closing the result, saved the number of rows, as the value won't be accesible after that
			$this->row_count=$this->num_rows;
			parent::close();
		}
	}

	/**
	 * Counts the number of rows in the result
	 *
	 * @return int
	 */
	public function count()
	{
		if(is_null($this->row_count))
			$this->row_count=$this->num_rows;
		return $this->row_count;
	}

	/**
	 * If an array, it checks the fields against a set of prefixes and splits the array into multiple sub-arrays, where
	 * key = prefix
	 * value = array containing fields with the prefix removed
	 *
	 * @var array
	 */
	public $split_by_prefixes=array();

	/**
	 * If non-null, array-retrieving operations will set the keys to the value of this field
	 *
	 * @var string
	 */
	public $index_field;

	/**
	 * Splits a result row into multiple arrays and removes prefixes from the field names
	 *
	 * @param array $row Result row
	 * @return array The same row split into sub-arrays
	 */
	protected function split_Row($row)
	{
		if(count($this->split_by_prefixes)==0 || !is_array($row))
			return $row;
		$containers=array();
		foreach($row as $key=>&$value) {
			// search container keys
			foreach($this->split_by_prefixes as $container_key=>$prefix) {
				$prefix_len=strlen($prefix);
				if(substr($key,0,$prefix_len)==$prefix) {
					if(is_numeric($container_key))
						$container_key=$prefix;
					$containers[$container_key][substr($key,$prefix_len)]=$value;
					unset($row[$key]);
				}
			}
		}
		return array_merge($row,$containers);
	}

	/**
	 * Fetch a result row as an associative array, optionally split into sub-arrays
	 *
	 * @return array
	 * @see mysqli_result::fetch_assoc();
	 */
	public function fetch_assoc()
	{
		$row = parent::fetch_assoc();
		if(!is_null($this-> index_field))
			$this-> cr_key = $row[$this-> index_field];
		return $this-> split_Row($row);
	}

	/**
	 * Returns one row as an array, optionally split into sub-arrays
	 * @deprecated Use extended fetch_assoc instead, with the split_by_prefixes functionality
	 *
	 * @return array
	 */
	public function get_Row($split_by_prefixes = null)
	{
		if(!is_null($split_by_prefixes))
			$this->split_by_prefixes=$split_by_prefixes;
		return $this->split_Row($this->fetch_assoc());
	}

	/**
	 * Returns all rows left in the result, optionally split into sub-arrays.
	 * @deprecated Use foreach() to use the iterators (more efficient)
	 *
	 * @param string $index_field
	 * @param array $split_by_prefixes
	 * @return array
	 */
	public function &get_Table($index_field = null, $split_by_prefixes = null)
	{
		if(!is_null($split_by_prefixes))
			$this->split_by_prefixes=$split_by_prefixes;
		if(!is_null($index_field))
			$this->index_field=$index_field;
		$array=array();
		foreach($this as $key=>$row)
			$array[$key]=$row;
		return $array;
	}

}
